Extract Pauli flow from XZ-corrections (closes #432)#526
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #526 +/- ##
==========================================
+ Coverage 89.18% 89.45% +0.26%
==========================================
Files 49 49
Lines 7354 7442 +88
==========================================
+ Hits 6559 6657 +98
+ Misses 795 785 -10 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
thierry-martinez
left a comment
There was a problem hiding this comment.
Thank you very much for this clean solution. Here are my initial comments on your PR.
|
Thanks for the thoughtful review, @thierry-martinez! Pushed 9a73dc7 addressing each point:
Let me know if you’d like any other changes. |
|
Hello - you are currently over the PR limit for unitaryHACK, so your PRs will be disregarded until you align with the rules. Reach out to hack@unitary.foundation to be reinstated. |
matulni
left a comment
There was a problem hiding this comment.
Thanks for this nice work! Here are some comments from my first pass. There's some work before merging, but in the mean time I'm ok with assigning you the issue.
For my own curiosity, would you be willing to share the promtps and skills (or even the reasoning log) of the LLM you used to code this ? This is of course not mandatory, but I'm curious to see how the solution was found. Thanks!
| See :meth:`XZCorrections.to_pauli_flow`. | ||
| """ | ||
| og = xz.og | ||
| adjacency: dict[int, set[int]] = {n: set(og.graph.neighbors(n)) for n in og.graph.nodes} |
There was a problem hiding this comment.
| adjacency: dict[int, set[int]] = {n: set(og.graph.neighbors(n)) for n in og.graph.nodes} | |
| adjacency: dict[int, set[int]] = {n: og.neighbors({n}) for n in og.graph.nodes} |
| solution = solve_f2_linear_system(lhs, MatGF2(b)) | ||
|
|
||
| correction_set = set(fixed_in_p) | ||
| correction_set.update(v for v, bit in zip(variables, solution, strict=True) if int(bit)) |
There was a problem hiding this comment.
| correction_set.update(v for v, bit in zip(variables, solution, strict=True) if int(bit)) | |
| correction_set.update(v for v, bit in zip(variables, solution, strict=True) if bit) |
| candidates = sorted(a for a in nonfuture_others if a in non_inputs and labels.get(a) in {Axis.X, Axis.Y}) | ||
| variables = [*candidates, node] if self_is_var else list(candidates) |
There was a problem hiding this comment.
I don't think sorting is necessary, but if it is, please add a test which would fail if candidates are not sorted.
| candidates = sorted(a for a in nonfuture_others if a in non_inputs and labels.get(a) in {Axis.X, Axis.Y}) | |
| variables = [*candidates, node] if self_is_var else list(candidates) | |
| variables = [a for a in nonfuture_others | {node} if a in non_inputs and labels.get(a) in {Axis.X, Axis.Y}] |
With the suggestion above, self_is_var is no longer necessary.
| _assert_round_trip(pattern) | ||
|
|
||
|
|
||
| @pytest.mark.filterwarnings("ignore:Open graph with non-inferred Pauli measurements.") |
There was a problem hiding this comment.
I believe that such warnings should not be triggered here; and if they are, we should address them, because whether Pauli measurements are inferred (or not) affects the Pauli flow.
| ] | ||
|
|
||
|
|
||
| @pytest.mark.filterwarnings("ignore:Open graph with non-inferred Pauli measurements.") |
There was a problem hiding this comment.
Same comment as above: these warnings should not be triggered, and they are relevant with respect to Pauli flows.
…check Responds to @thierry-martinez's review on TeamGraphix#526: - Remove the two @pytest.mark.filterwarnings("ignore:Open graph with non-inferred Pauli measurements.") suppressions. The warning does not actually fire for these tests (verified by promoting it to an error), so the suppressions were unnecessary and masked a semantically relevant signal. - Stop running pf.check_well_formed() systematically in production inside XZCorrections.to_pauli_flow: the flow is well formed by construction, and the sanity check is exercised in the test-suite via is_well_formed(). This also avoids the production manifestation of the empty-partial-order bug (TeamGraphix#531): Pattern().extract_xzcorrections().to_pauli_flow() no longer raises PartialOrderError. - Add test_to_pauli_flow_empty_pattern covering that empty-pattern case. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Thanks, @thierry-martinez — both points addressed in the latest commit. Non-inferred Pauli warning suppressions. You're right that these shouldn't be silenced. I removed both
This also removes the production manifestation of #531: |
|
Thanks for the careful review, @matulni — all addressed in the latest push. Reconstruction (
Typed error. Added Defensive check. Removed Tests.
On the LLM question — I did use an AI coding assistant (Claude) and then verified everything myself by running the full test suite, which is how the edge cases (the anachronical corrections, the empty-pattern path) got shaken out. I'd rather keep my specific prompts and workflow to myself, but appreciate the curiosity — and I'm glad to keep iterating on the PR the normal way. |
|
Hi @thierry-martinez @matulni — quick ready-for-re-review ping with the unitaryHACK deadline tomorrow. I addressed your Jun 10 review feedback in my Jun 10–11 replies. CI is fully green (24/24) and mergeable state is clean. Whenever you have a moment, would appreciate a final look — happy to make any last tweaks if anything else needs attention. Thanks for the careful review on this one! |
thierry-martinez
left a comment
There was a problem hiding this comment.
After careful consideration, we've decided to award you the bounty for this PR as well. Please leave a comment on issue #432 so we can assign it to you. In the meantime, take a look at the code‑coverage results: https://app.codecov.io/gh/TeamGraphix/graphix/pull/526
- Assert the rendered FlowGenericError(NoPauliFlow) message in the existing no-flow test, exercising the corresponding branch in exceptions.py. - Mark the empty-constraint-system early return in _solve_pauli_correction_set with `# pragma: no cover`: it is a defensive guard that is unreachable for a measured node, since the output nodes are maximally future and the future loop therefore always contributes at least one row. The guard is kept so the GF(2) reduction never runs on an empty matrix. Closes the remaining patch-coverage gap reported by Codecov on TeamGraphix#526.
matulni
left a comment
There was a problem hiding this comment.
Thanks for the nice job, LGTM!
(Please wait for a second approval).
| for i in range(lhs.shape[0]): | ||
| if not lhs[i].any() and b[i] != 0: | ||
| return None |
There was a problem hiding this comment.
| for i in range(lhs.shape[0]): | |
| if not lhs[i].any() and b[i] != 0: | |
| return None | |
| for coeffs, const in zip(lhs, b, strict=True): | |
| if not np.any(coeffs) and const: | |
| return None |
There was a problem hiding this comment.
Applied in e7fa8ab — thanks, that reads much better. Iterating the rows directly with zip(lhs, b, strict=True) and np.any drops the index bookkeeping entirely. And thanks for the approval!
|
Hi @Vinny010, before we merge could you please update the CHANGELOG? |
thierry-martinez
left a comment
There was a problem hiding this comment.
Sorry for the delay; I am still checking that the test suite is comprehensive enough.
One problem I have with the current test suite is that the following implementation of XZCorrections.to_pauli_flow passes all the tests:
def to_pauli_flow(self) -> PauliFlow[_AM_co]:
pf = self.og.find_pauli_flow()
if pf is None:
raise FlowGenericError(FlowGenericErrorReason.NoPauliFlow)
return pfIt is obvious that this implementation is incorrect: the Pauli flow implemented by the pattern is not necessarily the same as the Pauli flow inferred from the underlying open graph, since the Pauli flow is not unique.
Could you come up with a test that exercises this property?
Implement `XZCorrections.to_pauli_flow` and the convenience method `Pattern.extract_pauli_flow`, reconstructing a Pauli flow directly from a pattern's XZ-corrections (Theorem 4 of Browne et al. 2007) rather than from the underlying open graph (whose Pauli flow is not unique and need not generate the pattern). The difficulty is the anachronical corrections: corrections targeting X/Y Pauli-measured nodes in the present or past of the corrected node. These are dropped by `PauliFlow.to_corrections` (the `& future` filter) and so never appear in the pattern, so they must be reconstructed. For each measured node this is cast as a GF(2) linear system: the future membership of the correction set is pinned by the observed X-corrections; the free variables are the anachronical (non-future, X/Y-measured) candidates and, where allowed, the node itself; and the equations encode the odd-neighbourhood constraints (Z-corrections on future nodes, P2 on past non-(Y/Z) nodes, the P3 coupling on past Y nodes, and the local proposition P4-P9 on the node). The system is solved over GF(2) with `_solve_gf2`. Tests verify, on the three worked examples of the issue, on a Pauli-measured open graph, and on a randomized family of open graphs that admit a Pauli flow, that the reconstructed flow is well formed and that `to_corrections()` reproduces the pattern's corrections exactly (the decisive round-trip criterion). Passes ruff, mypy --strict, pyright, and pytest locally. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cover the branches where no Pauli flow is compatible with the XZ-corrections: a measured input node that must correct itself, and an isolated XY-measured node whose proposition P4 cannot be satisfied (unsolvable GF(2) system). Addresses the patch-coverage gap reported on the PR. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Reuse `graphix._linalg.solve_f2_linear_system` rather than the ad-hoc `_solve_gf2` (which is removed). The per-node augmented matrix `[A | b]` is reduced to row echelon form with `MatGF2.gauss_elimination(ncols=n_vars)`, inconsistency is detected by scanning for `[0...0 | 1]` rows, and the reduced system is then handed to the existing solver. - Update the `XZCorrections.to_pauli_flow` docstring to point at the existing GF(2) solver and to spell out the propositions encoded by the GF(2) system (P1 by construction via the X/Y-axis candidate restriction; P2-P9 directly). - Add a comment on the `pf.check_well_formed()` call: it is a regression guard; the algorithm satisfies the propositions by construction, so a failure there would indicate a bug rather than malformed input. - In the randomized round-trip test, narrow `except Exception` to `OpenGraphError` (the only documented raise of `OpenGraph.to_pattern` when no flow exists) and drop the cosmetic `round` on the random measurement angles. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…check Responds to @thierry-martinez's review on TeamGraphix#526: - Remove the two @pytest.mark.filterwarnings("ignore:Open graph with non-inferred Pauli measurements.") suppressions. The warning does not actually fire for these tests (verified by promoting it to an error), so the suppressions were unnecessary and masked a semantically relevant signal. - Stop running pf.check_well_formed() systematically in production inside XZCorrections.to_pauli_flow: the flow is well formed by construction, and the sanity check is exercised in the test-suite via is_well_formed(). This also avoids the production manifestation of the empty-partial-order bug (TeamGraphix#531): Pattern().extract_xzcorrections().to_pauli_flow() no longer raises PartialOrderError. - Add test_to_pauli_flow_empty_pattern covering that empty-pattern case. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…trized tests - core.py: simplify the GF(2) variable selection (drop `self_is_var`/sorting), merge the P2/P3 constraint loops into one with an explanatory comment, add a general comment describing what `matrix`/`rhs` encode, and tidy the `adjacency`/`solve_f2_linear_system`/`correction_set.update` lines. - exceptions.py: add `FlowGenericErrorReason.NoPauliFlow`; raise the typed `FlowGenericError(NoPauliFlow)` instead of a bare `FlowError` when no Pauli flow reconciles the XZ-corrections. - tests: parametrize the randomized round-trip over the seed (one case per seed, skipping draws with no edges / no flow) and parametrize the no-flow failure cases (asserting the typed reason), adding XZ/YZ-input cases. - test_pattern.py: mirror the causal/gflow extraction tests for the Pauli flow (`test_extract_pauli_flow_rnd_circuit` and `test_extract_pauli_flow`). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Assert the rendered FlowGenericError(NoPauliFlow) message in the existing no-flow test, exercising the corresponding branch in exceptions.py. - Mark the empty-constraint-system early return in _solve_pauli_correction_set with `# pragma: no cover`: it is a defensive guard that is unreachable for a measured node, since the output nodes are maximally future and the future loop therefore always contributes at least one row. The guard is kept so the GF(2) reduction never runs on an empty matrix. Closes the remaining patch-coverage gap reported by Codecov on TeamGraphix#526.
…puts and Y/Z-measured nodes
The suggested reduction (skipping the future-node equation for outputs and Y/Z-measured nodes) is not equivalent: when a Z-correction targets an output node in the future, dropping that GF(2) equation breaks the Z-correction round-trip. Reverted to the full future loop and added a deterministic regression test (test_extract_pauli_flow_output_zcorrection) that round-trips on the correct code and fails under the reduction.
Add test_extract_pauli_flow_pins_the_pattern_specific_flow.
The existing round-trip tests all use patterns whose Pauli flow happens
to coincide with the maximally-delayed flow returned by
OpenGraph.find_pauli_flow, so a trivial implementation that delegates to
find_pauli_flow and ignores the XZ-corrections of the pattern entirely
passes every existing case.
The new test pins down a small open graph (path 0-1-2-3, all X-measured,
output 3) that admits two well-formed Pauli flows whose to_corrections()
outputs genuinely differ -- one with an anachronical-only correction
{1} for node 0, one with the future correction {1, 3} -- builds the
XZ-corrections of each, and asserts that the reconstruction returns the
chosen flow, not the one find_pauli_flow would have returned.
27fd0b7 to
377b25b
Compare
|
Thanks @thierry-martinez — addressed in 377b25b on the rebased branch. You were right that the existing round-trip tests don't distinguish the real reconstruction from the trivial The new test
It then builds the XZ-corrections of each, asserts the reconstruction returns the chosen flow exactly, and asserts both round-trip back to their original corrections. The discriminator: any trivial impl that ignores the XZ-corrections returns the same flow for both inputs, so it can match at most one of them — the assertion fires for whichever case the trivial impl misses. I verified this empirically by monkey-patching the trivial impl in: with that swap, the five existing scalar tests ( Also: rebased the branch onto current |
Closes #432.
Implements
XZCorrections.to_pauli_flowand the convenience methodPattern.extract_pauli_flow(analogous toextract_causal_flow/extract_gflow), reconstructing a Pauli flow directly from the pattern's XZ-corrections (Theorem 4 of Browne et al. 2007) rather than from the underlying open graph — whose Pauli flow is not unique and is not guaranteed to generate the pattern.How the anachronical corrections were tackled (the crux of the issue)
The hard part, as the issue notes, is that a Pauli flow's correction sets may contain anachronical corrections: corrections targeting X/Y Pauli-measured nodes that lie in the present or past of the corrected node.
PauliFlow.to_correctionsdiscards these (thecorrecting_set & futurefilter), so they never appear in the pattern and cannot be read off the corrections — they must be reconstructed.For each measured node
i, reconstruction is cast as a linear system over GF(2):i, membership in the correction setp(i)is fixed by the observed X-corrections ofi(and must reproduce the Z-corrections via the odd neighbourhood). These become constants of the system.p(i)by P1) — and, where the local proposition allows it,iitself.i(P4–P9, handling the XY/XZ/YZ planes and the X/Y/Z axes).The system is solved with a small GF(2) Gaussian-elimination helper (
_solve_gf2); failure to solve at any node means no Pauli flow is compatible with the corrections. The resulting flow is then validated byPauliFlow.check_well_formed.Correctness
The decisive check is the round trip: for the reconstructed
pf,pf.to_corrections()must reproduce the pattern's X- and Z-corrections exactly (this is what guarantees it generates this pattern). Tests verify well-formedness and the round trip on:p(0) = {1, 3}, p(1) = {2}, p(2) = {3}etc., including the anachronical node1),There are also unit tests for the GF(2) solver. Passes
ruff,mypy --strict,pyright, andpytestlocally (new tests plus the existing flow / open-graph / pattern suites).Developed with LLM assistance, reviewed and tested by me.